import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

public class Shrinker {

	private DataBase dataBase;
	private int folds;
	private ArrayList<NearestCentroidClassifier> nearestCentroids;
	private NearestCentroidClassifier nearestCentroidOverall;

	public Shrinker(DataBase dataBase, int folds) throws Exception {
		this.nearestCentroids = new ArrayList<NearestCentroidClassifier>(folds);

		ArrayList<ArrayList<Sample>> samplesGroups = Utils.balancedDivision(dataBase, folds);

		int instances = dataBase.getSamples().size();

		for (int i = 0; i < folds; i++) {
			ArrayList<Sample> testSet = samplesGroups.get(i);
			ArrayList<Sample> trainingSet = new ArrayList<Sample>(instances - testSet.size());

			for (int j = 0; j < folds; j++) {
				if (i != j) {
					trainingSet.addAll(samplesGroups.get(j));
				}
			}

			NearestCentroidClassifier classifier = new NearestCentroidClassifier(trainingSet, testSet);
			classifier.calculateM();
			classifier.initializeThresholdScale();
			classifier.calculateD();

			nearestCentroids.add(classifier);
		}

		this.nearestCentroidOverall = new NearestCentroidClassifier(dataBase.getSamples(), new ArrayList<Sample>());
		this.nearestCentroidOverall.calculateM();
		this.nearestCentroidOverall.initializeThresholdScale();
		this.nearestCentroidOverall.calculateD();

		this.dataBase = dataBase;
		this.folds = folds;
	}

	public double softThresholding(double d, double delta) {
		return Math.signum(d) * Math.max((Math.abs(d) - delta), 0);
	}

	public HashMap<String, Centroid> shrinkCentroids(NearestCentroidClassifier nearestCentroid, double delta) {
		HashMap<String, Centroid> classCentroids = nearestCentroid.getCentroids();

		HashMap<String, Double> m = nearestCentroid.getM();
		HashMap<String, ArrayList<Double>> d = nearestCentroid.getD();

		@SuppressWarnings("unchecked")
		HashMap<String, Centroid> shrunkenCentroids = (HashMap<String, Centroid>) classCentroids.clone();

		Set<String> classes = classCentroids.keySet();

		ArrayList<Double> overallValues = nearestCentroid.getOverallCentroid().getValues();

		for (String classeK : classes) {
			Centroid shrunkenCentroid = (Centroid) shrunkenCentroids.get(classeK).clone();
			shrunkenCentroids.put(classeK, shrunkenCentroid);
			ArrayList<Double> centroidValues = shrunkenCentroid.getValues();

			ArrayList<Double> dk = d.get(classeK);
			double mk = m.get(classeK);

			for (int i = 0; i < centroidValues.size(); i++) {
				double xi = overallValues.get(i);
				double dik = dk.get(i);
				double newValue = xi + (mk * nearestCentroid.getS(i) * this.softThresholding(dik, delta));
				centroidValues.set(i, newValue);
			}
		}

		return shrunkenCentroids;
	}

	public double crossValidation(double delta) throws Exception {
		double cv = 0;

		for (NearestCentroidClassifier classifier : this.nearestCentroids) {
			HashMap<String, Centroid> originalCentroids = classifier.getCentroids();
			HashMap<String, Centroid> shrunkenCentroids = this.shrinkCentroids(classifier, delta);
			classifier.setCentroids(shrunkenCentroids);
			cv += classifier.classifyTestSet();
			classifier.setCentroids(originalCentroids);
		}

		return cv / this.folds;
	}

	public double searchDelta() throws Exception {
		double delta = 0;
		double increase = 5.0;
		double halfIncrease = increase / 2;

		double unshrunkenError = this.crossValidation(0);

		int i = 0;
		while (halfIncrease != increase && halfIncrease != 0 && i < 100) {
			double shrunkenError = this.crossValidation(delta + increase);

			if (shrunkenError <= unshrunkenError) {
				delta += increase;
			} else {
				increase = halfIncrease;
				halfIncrease = increase / 2.0;
			}
			i++;
		}

		return delta;
	}

	public Set<Integer> discriminantAttributes(HashMap<String, Centroid> centroids) {
		ArrayList<Double> overallValues = this.nearestCentroidOverall.getOverallCentroid().getValues();
		int nAttrs = overallValues.size();

		Set<Integer> discriminantAttributes = new HashSet<Integer>();

		ArrayList<HashSet<Double>> diffValues = new ArrayList<HashSet<Double>>(nAttrs);
		for (int i = 0; i < nAttrs; i++) {
			HashSet<Double> set = new HashSet<Double>();
			set.add(overallValues.get(i));
			diffValues.add(set);
		}

		for (Centroid centroid : centroids.values()) {
			ArrayList<Double> centroidValues = centroid.getValues();
			for (int i = 0; i < centroidValues.size(); i++) {
				diffValues.get(i).add(centroidValues.get(i));
			}
		}

		for (int i = 0; i < diffValues.size(); i++) {
			if (diffValues.get(i).size() > 1) {
				discriminantAttributes.add(i);
			}
		}

		return discriminantAttributes;
	}

	public DataBase getDataBase() {
		return dataBase;
	}

	public void setDataBase(DataBase dataBase) {
		this.dataBase = dataBase;
	}

	public int getFolds() {
		return folds;
	}

	public void setFolds(int folds) {
		this.folds = folds;
	}

	public ArrayList<NearestCentroidClassifier> getNearestCentroids() {
		return nearestCentroids;
	}

	public void setNearestCentroids(ArrayList<NearestCentroidClassifier> nearestCentroids) {
		this.nearestCentroids = nearestCentroids;
	}

	public NearestCentroidClassifier getNearestCentroidOverall() {
		return nearestCentroidOverall;
	}

	public void setNearestCentroidOverall(NearestCentroidClassifier nearestCentroidOverall) {
		this.nearestCentroidOverall = nearestCentroidOverall;
	}
}
